/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.x.component.hardware;

import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.text.format.Formatter;
import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.util.TimeUtils;

import com.android.internal.app.ProcessMap;
import com.android.internal.app.procstats.DumpUtils;
import com.android.internal.app.procstats.IProcessStats;
import com.android.internal.app.procstats.ProcessState;
import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.app.procstats.ProcessStats.ProcessDataCollection;
import com.android.internal.app.procstats.ProcessStats.TotalMemoryUseCollection;
import com.android.internal.app.procstats.ServiceState;
import com.android.internal.util.MemInfoReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ProcStatsData {

	private static final String TAG = "ProcStatsManager";

	private static final boolean DEBUG = true;

	private static ProcessStats sStatsXfer;

	private PackageManager mPm;
	private Context mContext;
	private long memTotalTime;

	private IProcessStats mProcessStats;
	private ProcessStats mStats;

	private boolean mUseUss;
	private long mDuration;

	private int[] mMemStates;

	private int[] mStates;

	private MemInfo mMemInfo;

	private ArrayList<ProcStatsPackageEntry> pkgEntries;

	public ProcStatsData(Context context, boolean useXfer) {
		mContext = context;
		mPm = context.getPackageManager();
		mProcessStats = IProcessStats.Stub.asInterface(ServiceManager.getService(ProcessStats.SERVICE_NAME));
		mMemStates = ProcessStats.ALL_MEM_ADJ;
		mStates = ProcessStats.BACKGROUND_PROC_STATES;
		if (useXfer) {
			mStats = sStatsXfer;
		}
	}

	public void setTotalTime(int totalTime) {
		memTotalTime = totalTime;
	}

	public void xferStats() {
		sStatsXfer = mStats;
	}

	public void setMemStates(int[] memStates) {
		mMemStates = memStates;
		refreshStats(false);
	}

	public void setStats(int[] stats) {
		this.mStates = stats;
		refreshStats(false);
	}

	public int getMemState() {
		int factor = mStats.mMemFactor;
		if (factor == ProcessStats.ADJ_NOTHING) {
			return ProcessStats.ADJ_MEM_FACTOR_NORMAL;
		}
		if (factor >= ProcessStats.ADJ_SCREEN_ON) {
			factor -= ProcessStats.ADJ_SCREEN_ON;
		}
		return factor;
	}

	public MemInfo getMemInfo() {
		return mMemInfo;
	}

	public long getElapsedTime() {
		return mStats.mTimePeriodEndRealtime - mStats.mTimePeriodStartRealtime;
	}

	public void setDuration(long duration) {
		if (duration != mDuration) {
			mDuration = duration;
			refreshStats(true);
		}
	}

	public long getDuration() {
		return mDuration;
	}

	public List<ProcStatsPackageEntry> getEntries() {
		return pkgEntries;
	}

	public void refreshStats(boolean forceLoad) {
		if (mStats == null || forceLoad) {
			load();
		}

		pkgEntries = new ArrayList<>();

		long now = SystemClock.uptimeMillis();

		memTotalTime = DumpUtils.dumpSingleTime(null, null, mStats.mMemFactorDurations, mStats.mMemFactor,
				mStats.mStartTime, now);

		ProcessStats.TotalMemoryUseCollection totalMem = new ProcessStats.TotalMemoryUseCollection(
				ProcessStats.ALL_SCREEN_ADJ, mMemStates);
		mStats.computeTotalMemoryUse(totalMem, now);

		mMemInfo = new MemInfo(mContext, totalMem, memTotalTime);

		ProcessDataCollection bgTotals = new ProcessDataCollection(ProcessStats.ALL_SCREEN_ADJ, mMemStates, mStates);
		ProcessDataCollection runTotals = new ProcessDataCollection(ProcessStats.ALL_SCREEN_ADJ, mMemStates,
				ProcessStats.NON_CACHED_PROC_STATES);

		createPkgMap(getProcs(bgTotals, runTotals), bgTotals, runTotals);
		if (totalMem.sysMemZRamWeight > 0 && !totalMem.hasSwappedOutPss) {
			distributeZRam(totalMem.sysMemZRamWeight);
		}

		ProcStatsPackageEntry osPkg = createOsEntry(bgTotals, runTotals, totalMem, mMemInfo.baseCacheRam);
		pkgEntries.add(osPkg);
	}

	private void createPkgMap(ArrayList<ProcStatsEntry> procEntries, ProcessDataCollection bgTotals,
			ProcessDataCollection runTotals) {
		// Combine processes into packages.
		ArrayMap<String, ProcStatsPackageEntry> pkgMap = new ArrayMap<>();
		for (int i = procEntries.size() - 1; i >= 0; i--) {
			ProcStatsEntry proc = procEntries.get(i);
			proc.evaluateTargetPackage(mPm, mStats, bgTotals, runTotals, sEntryCompare, mUseUss);
			ProcStatsPackageEntry pkg = pkgMap.get(proc.mBestTargetPackage);
			if (pkg == null) {
				pkg = new ProcStatsPackageEntry(proc.mBestTargetPackage, memTotalTime);
				pkgMap.put(proc.mBestTargetPackage, pkg);
				pkgEntries.add(pkg);
			}
			pkg.addEntry(proc);
		}
	}

	private void distributeZRam(double zramWeight) {
		// Distribute kernel's Z-Ram across processes, based on how much they have been
		// running.
		// The idea is that the memory used by the kernel for this is not really the
		// kernel's
		// responsibility, but that of whoever got swapped in to it... and we will take
		// how
		// much a process runs for as a sign of the proportion of Z-Ram it is
		// responsible for.

		long zramMem = (long) (zramWeight / memTotalTime);
		long totalTime = 0;
		for (int i = pkgEntries.size() - 1; i >= 0; i--) {
			ProcStatsPackageEntry entry = pkgEntries.get(i);
			for (int j = entry.mEntries.size() - 1; j >= 0; j--) {
				ProcStatsEntry proc = entry.mEntries.get(j);
				totalTime += proc.mRunDuration;
			}
		}
		for (int i = pkgEntries.size() - 1; i >= 0 && totalTime > 0; i--) {
			ProcStatsPackageEntry entry = pkgEntries.get(i);
			long pkgRunTime = 0;
			long maxRunTime = 0;
			for (int j = entry.mEntries.size() - 1; j >= 0; j--) {
				ProcStatsEntry proc = entry.mEntries.get(j);
				pkgRunTime += proc.mRunDuration;
				if (proc.mRunDuration > maxRunTime) {
					maxRunTime = proc.mRunDuration;
				}
			}
			long pkgZRam = (zramMem * pkgRunTime) / totalTime;
			if (pkgZRam > 0) {
				zramMem -= pkgZRam;
				totalTime -= pkgRunTime;
				ProcStatsEntry procEntry = new ProcStatsEntry(entry.mPackage, 0, "Z-Ram", maxRunTime, pkgZRam,
						memTotalTime);
				procEntry.evaluateTargetPackage(mPm, mStats, null, null, sEntryCompare, mUseUss);
				entry.addEntry(procEntry);
			}
		}
	}

	private ProcStatsPackageEntry createOsEntry(ProcessDataCollection bgTotals, ProcessDataCollection runTotals,
			TotalMemoryUseCollection totalMem, long baseCacheRam) {
		// Add in fake entry representing the OS itself.
		ProcStatsPackageEntry osPkg = new ProcStatsPackageEntry("os", memTotalTime);
		ProcStatsEntry osEntry;
		if (totalMem.sysMemNativeWeight > 0) {
			osEntry = new ProcStatsEntry("os", 0, "Native", memTotalTime,
					(long) (totalMem.sysMemNativeWeight / memTotalTime), memTotalTime);
			osEntry.evaluateTargetPackage(mPm, mStats, bgTotals, runTotals, sEntryCompare, mUseUss);
			osPkg.addEntry(osEntry);
		}
		if (totalMem.sysMemKernelWeight > 0) {
			osEntry = new ProcStatsEntry("os", 0, "Kernel", memTotalTime,
					(long) (totalMem.sysMemKernelWeight / memTotalTime), memTotalTime);
			osEntry.evaluateTargetPackage(mPm, mStats, bgTotals, runTotals, sEntryCompare, mUseUss);
			osPkg.addEntry(osEntry);
		}
		/*
		 * Turned off now -- zram is being distributed across running apps. if
		 * (totalMem.sysMemZRamWeight > 0) { osEntry = new ProcStatsEntry("os", 0,
		 * mContext.getString(R.string.process_stats_os_zram), memTotalTime, (long)
		 * (totalMem.sysMemZRamWeight / memTotalTime));
		 * osEntry.evaluateTargetPackage(mPm, mStats, bgTotals, runTotals,
		 * sEntryCompare, mUseUss); osPkg.addEntry(osEntry); }
		 */
		if (baseCacheRam > 0) {
			osEntry = new ProcStatsEntry("os", 0, "Caches", memTotalTime, baseCacheRam / 1024, memTotalTime);
			osEntry.evaluateTargetPackage(mPm, mStats, bgTotals, runTotals, sEntryCompare, mUseUss);
			osPkg.addEntry(osEntry);
		}
		return osPkg;
	}

	public static String makeDuration(long time) {
		StringBuilder sb = new StringBuilder(32);
		TimeUtils.formatDuration(time, sb);
		return sb.toString();
	}

	private ArrayList<ProcStatsEntry> getProcs(ProcessDataCollection bgTotals, ProcessDataCollection runTotals) {
		final ArrayList<ProcStatsEntry> procEntries = new ArrayList<>();
		if (DEBUG)
			Log.d(TAG, "-------------------- PULLING PROCESSES");

		final ProcessMap<ProcStatsEntry> entriesMap = new ProcessMap<ProcStatsEntry>();
		for (int ipkg = 0, N = mStats.mPackages.getMap().size(); ipkg < N; ipkg++) {
			final SparseArray<LongSparseArray<ProcessStats.PackageState>> pkgUids = mStats.mPackages.getMap()
					.valueAt(ipkg);
			for (int iu = 0; iu < pkgUids.size(); iu++) {
				final LongSparseArray<ProcessStats.PackageState> vpkgs = pkgUids.valueAt(iu);
				for (int iv = 0; iv < vpkgs.size(); iv++) {
					final ProcessStats.PackageState st = vpkgs.valueAt(iv);
					for (int iproc = 0; iproc < st.mProcesses.size(); iproc++) {
						final ProcessState pkgProc = st.mProcesses.valueAt(iproc);
						final ProcessState proc = mStats.mProcesses.get(pkgProc.getName(), pkgProc.getUid());
						if (proc == null) {
							Log.w(TAG, "No process found for pkg " + st.mPackageName + "/" + st.mUid + " proc name "
									+ pkgProc.getName());
							continue;
						}
						ProcStatsEntry ent = entriesMap.get(proc.getName(), proc.getUid());
						if (ent == null) {
							ent = new ProcStatsEntry(proc, st.mPackageName, bgTotals, runTotals, mUseUss);
							if (ent.mRunWeight > 0) {
								if (DEBUG)
									Log.d(TAG,
											"Adding proc " + proc.getName() + "/" + proc.getUid() + ": time="
													+ makeDuration(ent.mRunDuration) + " ("
													+ ((((double) ent.mRunDuration) / memTotalTime) * 100) + "%)"
													+ " pss=" + ent.mAvgRunMem);
								entriesMap.put(proc.getName(), proc.getUid(), ent);
								procEntries.add(ent);
							}
						} else {
							ent.addPackage(st.mPackageName);
						}
					}
				}
			}
		}

		if (DEBUG)
			Log.d(TAG, "-------------------- MAPPING SERVICES");

		// Add in service info.
		for (int ip = 0, N = mStats.mPackages.getMap().size(); ip < N; ip++) {
			SparseArray<LongSparseArray<ProcessStats.PackageState>> uids = mStats.mPackages.getMap().valueAt(ip);
			for (int iu = 0; iu < uids.size(); iu++) {
				LongSparseArray<ProcessStats.PackageState> vpkgs = uids.valueAt(iu);
				for (int iv = 0; iv < vpkgs.size(); iv++) {
					ProcessStats.PackageState ps = vpkgs.valueAt(iv);
					for (int is = 0, NS = ps.mServices.size(); is < NS; is++) {
						ServiceState ss = ps.mServices.valueAt(is);
						if (ss.getProcessName() != null) {
							ProcStatsEntry ent = entriesMap.get(ss.getProcessName(), uids.keyAt(iu));
							if (ent != null) {
								if (DEBUG)
									Log.d(TAG, "Adding service " + ps.mPackageName + "/" + ss.getName() + "/"
											+ uids.keyAt(iu) + " to proc " + ss.getProcessName());
								ent.addService(ss);
							} else {
								Log.w(TAG, "No process " + ss.getProcessName() + "/" + uids.keyAt(iu) + " for service "
										+ ss.getName());
							}
						}
					}
				}
			}
		}

		return procEntries;
	}

	private void load() {
		try {
			ParcelFileDescriptor pfd = mProcessStats.getStatsOverTime(mDuration);
			mStats = new ProcessStats(false);
			InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
			mStats.read(is);
			try {
				is.close();
			} catch (IOException e) {
			}
			if (mStats.mReadError != null) {
				Log.w(TAG, "Failure reading process stats: " + mStats.mReadError);
			}
		} catch (RemoteException e) {
			Log.e(TAG, "RemoteException:", e);
		}
	}

	public static class RomInfo {
		public long realUsedRom;
		public long realFreeRom;
		public long realTotalRom;
		public String usedRom;
		public String freeRom;
		public String totalRom;
		long baseCacheRam;

		public RomInfo(Context context) {

		}

	}

	public static class MemInfo {
		public double realUsedRam;
		public double realFreeRam;
		public double realTotalRam;
		public String uiUsedRam;
		public String uiFreeRam;
		public String uiTotalRam;
		long baseCacheRam;

		double[] mMemStateWeights = new double[ProcessStats.STATE_COUNT];
		double freeWeight;
		double usedWeight;
		double weightToRam;
		double totalRam;
		double totalScale;
		long memTotalTime;

		public double getWeightToRam() {
			return weightToRam;
		}

		private MemInfo(Context context, ProcessStats.TotalMemoryUseCollection totalMem, long memTotalTime) {
			this.memTotalTime = memTotalTime;
			calculateWeightInfo(context, totalMem, memTotalTime);

			double usedRam = (usedWeight * 1024) / memTotalTime;
			double freeRam = (freeWeight * 1024) / memTotalTime;
			totalRam = usedRam + freeRam;
			totalScale = realTotalRam / totalRam;
			weightToRam = totalScale / memTotalTime * 1024;

			realUsedRam = usedRam * totalScale;
			realFreeRam = freeRam * totalScale;
			if (DEBUG) {
				Log.i(TAG, "Scaled Used RAM: " + Formatter.formatShortFileSize(context, (long) realUsedRam));
				Log.i(TAG, "Scaled Free RAM: " + Formatter.formatShortFileSize(context, (long) realFreeRam));
			}
			if (DEBUG) {
				Log.i(TAG, "Adj Scaled Used RAM: " + Formatter.formatShortFileSize(context, (long) realUsedRam));
				Log.i(TAG, "Adj Scaled Free RAM: " + Formatter.formatShortFileSize(context, (long) realFreeRam));
			}

			ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
			((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo(memInfo);
			if (memInfo.hiddenAppThreshold >= realFreeRam) {
				realUsedRam = freeRam;
				realFreeRam = 0;
				baseCacheRam = (long) realFreeRam;
			} else {
				realUsedRam += memInfo.hiddenAppThreshold;
				realFreeRam -= memInfo.hiddenAppThreshold;
				baseCacheRam = memInfo.hiddenAppThreshold;
			}
		}

		private void calculateWeightInfo(Context context, TotalMemoryUseCollection totalMem, long memTotalTime) {
			MemInfoReader memReader = new MemInfoReader();
			memReader.readMemInfo();
			realTotalRam = memReader.getTotalSize();
			freeWeight = totalMem.sysMemFreeWeight + totalMem.sysMemCachedWeight;
			usedWeight = totalMem.sysMemKernelWeight + totalMem.sysMemNativeWeight;
			if (!totalMem.hasSwappedOutPss) {
				usedWeight += totalMem.sysMemZRamWeight;
			}
			for (int i = 0; i < ProcessStats.STATE_COUNT; i++) {
				if (i == ProcessStats.STATE_SERVICE_RESTARTING) {
					// These don't really run.
					mMemStateWeights[i] = 0;
				} else {
					mMemStateWeights[i] = totalMem.processStateWeight[i];
					if (i >= ProcessStats.STATE_HOME) {
						freeWeight += totalMem.processStateWeight[i];
					} else {
						usedWeight += totalMem.processStateWeight[i];
					}
				}
			}
			if (DEBUG) {
				Log.i(TAG, "Used RAM: "
						+ Formatter.formatShortFileSize(context, (long) ((usedWeight * 1024) / memTotalTime)));
				Log.i(TAG, "Free RAM: "
						+ Formatter.formatShortFileSize(context, (long) ((freeWeight * 1024) / memTotalTime)));
				Log.i(TAG, "Total RAM: " + Formatter.formatShortFileSize(context,
						(long) (((freeWeight + usedWeight) * 1024) / memTotalTime)));
			}
		}
	}

	final static Comparator<ProcStatsEntry> sEntryCompare = new Comparator<ProcStatsEntry>() {
		@Override
		public int compare(ProcStatsEntry lhs, ProcStatsEntry rhs) {
			if (lhs.mRunWeight < rhs.mRunWeight) {
				return 1;
			} else if (lhs.mRunWeight > rhs.mRunWeight) {
				return -1;
			} else if (lhs.mRunDuration < rhs.mRunDuration) {
				return 1;
			} else if (lhs.mRunDuration > rhs.mRunDuration) {
				return -1;
			}
			return 0;
		}
	};
}
